import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
/*
 * AbstractClassifier.java
 * Usage: IR HW4. this class serves as a super class of MNBClassifier and TNBClassifiers. It provides evaluate() and classify() method
 * which are the same for both classifier. It also contains the signiture of train() method which should be overridden in the subclasses.
 * Author:Guannan Zhang
 */
public abstract class AbstractClassifier {

	// a document is classified into a category
	protected HashMap<Integer, Integer> classifiedClasses;
	// a document is labeled into a set of categories
	protected HashMap<Integer, HashSet<Integer>> labeledClasses;
	// total number of test documents
	protected int testDocNum;
	// number of categories
	protected int classNum;
	// prior probability P(c)
	protected HashMap<Integer, Double> pc;
	// probability of terms in each category P(t|c)
	protected HashMap<Integer, HashMap<Integer, Double>> ptc;

	public AbstractClassifier() {

		classifiedClasses = new HashMap<Integer, Integer>();
		labeledClasses = new HashMap<Integer, HashSet<Integer>>();
		// totalClasses = new HashSet<Integer>();
		testDocNum = 0;
		pc = new HashMap<Integer, Double>();
		ptc = new HashMap<Integer, HashMap<Integer, Double>>();
	}
	
	
	/*
	 * using the labeled and classified data, evaluate() provide micro- and macor- average of precison, recall, accuracy and f1.
	 */
	public void evaluate() {
		// System.out.println(labeledClasses);
		// System.out.println(classifiedClasses);
		
		
		// documents which are classified into classes
		HashMap<Integer, HashSet<Integer>> classifiedClassesContainDocs = new HashMap<Integer, HashSet<Integer>>();
		// documents which are labeled into classes
		HashMap<Integer, HashSet<Integer>> labeledClassesContainDocs = new HashMap<Integer, HashSet<Integer>>();

		for (Iterator<Entry<Integer, Integer>> it = classifiedClasses
				.entrySet().iterator(); it.hasNext();) {
			Entry<Integer, Integer> e = it.next();
			int docID = e.getKey();
			int c = e.getValue();

			if (classifiedClassesContainDocs.containsKey(c)) {
				classifiedClassesContainDocs.get(c).add(docID);
			} else {
				HashSet<Integer> docs = new HashSet<Integer>();
				docs.add(docID);
				classifiedClassesContainDocs.put(c, docs);
			}

		}

		for (Iterator<Entry<Integer, HashSet<Integer>>> it = labeledClasses
				.entrySet().iterator(); it.hasNext();) {
			Entry<Integer, HashSet<Integer>> e = it.next();
			int docID = e.getKey();
			HashSet<Integer> classes = e.getValue();

			for (int c : classes) {
				if (labeledClassesContainDocs.containsKey(c)) {
					labeledClassesContainDocs.get(c).add(docID);
				} else {
					HashSet<Integer> docs = new HashSet<Integer>();
					docs.add(docID);
					labeledClassesContainDocs.put(c, docs);
				}
			}

		}

		// System.out.println(labeledClassesContainDocs);
		// System.out.println(classifiedClassesContainDocs);

		double psum = 0;
		double rsum = 0;
		double asum = 0;
		double fsum = 0;

		int tpsum = 0;
		int fpsum = 0;
		int fnsum = 0;
		int tnsum = 0;

		for (int c = 1; c <= classNum; c++) {
			// int c = it.next();

			HashSet<Integer> positiveDocs = classifiedClassesContainDocs.get(c);
			HashSet<Integer> trueDocs = labeledClassesContainDocs.get(c);

			int tp = 0;
			for (int d : positiveDocs) {
				if (trueDocs.contains(d)) {
					tp++;
				}
			}
			int fp = positiveDocs.size() - tp;
			int fn = trueDocs.size() - tp;
			int tn = testDocNum - tp - fp - fn;

			// System.out.println(c);
			// System.out.println(tp + " " + fp + " " + tn + " " + fn);

			tpsum += tp;
			fpsum += fp;
			fnsum += fn;
			tnsum += tn;

			double precision = (double) tp / (double) (tp + fp);
			double recall = (double) tp / (double) (tp + fn);
			double accuracy = (double) (tp + tn) / (double) (tp + tn + fp + fn);
			double f1 = (2.0 * precision * recall) / (precision + recall);

			// System.out.println(precision + "\t" + recall + "\t" + accuracy +
			// "\t" + f1 );

			psum += precision;
			rsum += recall;
			asum += accuracy;
			fsum += f1;

		}

		double macroPrecision = psum / (double) classNum;
		double macroRecall = rsum / (double) classNum;
		double macroAccuracy = asum / (double) classNum;
		double macroF1 = fsum / (double) classNum;

		System.out.println("macro-precision=" + macroPrecision + "\tmacro-recall=" + macroRecall + "\tmacro-accuracy="
				+ macroAccuracy + "\tmacro-f1=" + macroF1);

		double microPrecision = (double) tpsum / (double) (tpsum + fpsum);
		double microRecall = (double) tpsum / (double) (tpsum + fnsum);
		double microAccuracy = (double) (tpsum + tnsum)
				/ (double) (tpsum + tnsum + fpsum + fnsum);
		double microF1 = (2.0 * microPrecision * microRecall)
				/ (microPrecision + microRecall);
		// System.out.println(tpsum + " " + fpsum + " " + fnsum+ " " + tnsum);
		System.out.println("micro-precision=" + microPrecision + "\tmicro-recall=" + microRecall + "\tmicro-accuracy="
				+ microAccuracy + "\tmicro-f1=" + microF1);

	}
	
	
	/*
	 * based on the training parameter pt and ptc, classify() predicts labels for each test document, which are stored in classifiedClasses 
	 */
	public void classify(String file) {
		try {

			BufferedReader br = new BufferedReader(new FileReader(file));
			int docID = 0;
			String line;
			while ((line = br.readLine()) != null) {
				docID++;
				String[] str = line.split(" ");

				String[] classes = str[0].split(",");
				HashSet<Integer> docClasses = new HashSet<Integer>();
				for (int i = 0; i < classes.length; i++) {
					docClasses.add(Integer.parseInt(classes[i]));
				}

				labeledClasses.put(docID, docClasses);

				// HashMap<Integer, Double> scoreOfClass = new HashMap<Integer,
				// Double>();
				double maxScore = Double.NEGATIVE_INFINITY;
				int maxClass = 0;
				for (int c = 1; c <= classNum; c++) {
					
					double score = Math.log(pc.get(c));

					for (int j = 1; j < str.length - 2; j++) {
						// System.out.println(str[i]);
						String[] pair = str[j].split(":");
						int term = Integer.parseInt(pair[0]);
						double times = Double.parseDouble(pair[1]);

						score += Math.log(ptc.get(c).get(term)) * times;
					}
					//System.out.println(c);
					//System.out.println(score);

					if (score > maxScore) {
						maxScore = score;
						maxClass = c;
					}

				}
				classifiedClasses.put(docID, maxClass);

			}
			br.close();
			testDocNum = docID;
			// System.out.println(testDocNum);
			// System.out.println(labeledClasses);
			// System.out.println(classifiedClasses);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public abstract void train(String file);

}
